import numpy as np
import os
import shutil
from glob import glob
import pandas as pd
import plotly.graph_objs as go
from plotly.subplots import make_subplots
from datetime import datetime
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_columns', None)
from plotly.io import write_html
from RankUtils import *
from 盈亏分析器 import rise_fall_factor, hold_period, top_n, filter_before_params

root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
pickle_path = os.path.join(root_path, 'data', 'pickle_data')

# 'data/pickle_data/swap'
pickle_data = os.path.join(pickle_path, 'swap')
# 'data/factors/swap'
factor_pickle = os.path.join(root_path, 'data', 'factors', 'swap')


def merge_and_mark(_start, _end, _compound_name, _period, _single_symbol=""):
    """
    合并并标记选币数据
    如果single_symbol==""，则合并标记所有币种；如果single_symbol!=""，则合并标记单个币种，并在遍历后检查该币种是否存在
    :param _start:
    :param _end:
    :param _compound_name: 组份名称
    :param _period:
    :param _single_symbol:
    :return: 合并标记结果
    """
    # 读取持仓面板数据
    position_pickle_path = os.path.join(root_path, 'data/回测结果', '中性回放结果', f'{_compound_name}', '持仓面板数据.pkl')
    df = pd.read_pickle(position_pickle_path)
    df = df.reset_index()
    df.rename(columns={'display_time': 'candle_begin_time'}, inplace=True)
    df = df[df['candle_begin_time'] >= pd.to_datetime(_start)]
    df = df[df['candle_begin_time'] <= pd.to_datetime(_end)]
    selected_columns = ['candle_begin_time', 'symbol', '持仓数量', '持仓方向']
    df = df[selected_columns]
    df.sort_values(by='candle_begin_time', inplace=True)
    df.reset_index(drop=True, inplace=True)

    # 合并价格数据并标记买入卖出时间
    group1 = df.groupby(['symbol', '持仓方向'])
    res_list1 = []
    # 遍历分组
    for n, g in group1:
        coin = n[0][0:-4]
        if _single_symbol != "" and _single_symbol != coin:  # 处理单个币种并且coin不是要处理的币种
            continue
        # ===分组处理数据
        g.sort_values(by='candle_begin_time', inplace=True)
        # 计算前后K线的时间间隔
        g['span'] = g['candle_begin_time'].diff()
        # 标记换仓的开始时间
        g.loc[g['span'] != _period, 'start_time'] = g['candle_begin_time']
        g['start_time'].fillna(method='ffill', inplace=True)
        # 将start_time列转换为日期时间对象
        g['start_time'] = pd.to_datetime(g['start_time'])
        # 合并价格信息
        coin_df = pd.read_feather(pickle_data + '/' + f'{coin}' + '-USDT.pkl')
        print(f'合并{coin}-USDT数据')
        p_df = pd.merge(g, coin_df, on=['candle_begin_time', 'symbol'], how='left')
        p_df = p_df[['candle_begin_time', 'symbol', 'open', 'close', '持仓方向', '持仓数量', 'start_time']]
        # 按照不同的持仓时间段分类，并计算持仓列表
        hold = p_df.groupby('start_time').agg({'candle_begin_time': 'last'}).reset_index()
        hold['end_time'] = hold['candle_begin_time']
        hold = hold[['start_time', 'end_time']]
        p_df = pd.merge(p_df, hold, on=['start_time'], how='left')
        res_list1.append(p_df)
    if len(res_list1) == 0:
        print(f'处理币种[{_single_symbol}]在持仓面板中不存在')
        return pd.DataFrame()
    df = pd.concat(res_list1)
    # 标记持仓状态
    # 开仓
    condition1 = df['candle_begin_time'] == df['start_time']
    # 平仓
    condition2 = df['candle_begin_time'] == df['end_time']
    # 持仓
    condition3 = (df['candle_begin_time'] != df['start_time']) & (df['candle_begin_time'] != df['end_time'])
    # 标记开平持状态
    cc1 = df['持仓方向'] == -1
    cc2 = df['持仓方向'] == 1
    df['开平状态'] = '持仓'
    df.loc[condition1 & cc1, '开平状态'] = '开空'
    df.loc[condition1 & cc2, '开平状态'] = '开多'
    df.loc[condition2 & cc1, '开平状态'] = '平空'
    df.loc[condition2 & cc2, '开平状态'] = '平多'
    df.loc[condition1 & cc1 & condition2, '开平状态'] = '开空' + '平空'
    df.loc[condition1 & cc2 & condition2, '开平状态'] = '开多' + '平多'
    df.sort_values(by='candle_begin_time', inplace=True)
    df.reset_index(drop=True, inplace=True)

    return df


def top_interval(df, _topn):
    # 计算区间涨跌幅
    df0 = df.copy()
    t_df = df0.groupby(['symbol', 'start_time', '持仓方向']).agg({'end_time': 'max',
                                                                   'open': 'first',
                                                                   'close': 'last',
                                                                   }).reset_index()
    t_df = t_df.rename(columns={'open': '开仓价格', 'close': '清仓价格'})
    t_df['区间涨跌幅_%'] = (t_df['清仓价格'] / t_df['开仓价格'] - 1) * t_df['持仓方向'] * 100
    _df = t_df.nlargest(_topn, '区间涨跌幅_%')
    df_ = t_df.nsmallest(_topn, '区间涨跌幅_%')
    top_df = pd.concat([_df, df_], axis=0)
    top_df = top_df[['symbol', '区间涨跌幅_%', 'start_time', 'end_time', '持仓方向', '开仓价格', '清仓价格']]
    top_df.sort_values(by='区间涨跌幅_%', inplace=True)
    top_df.reset_index(drop=True, inplace=True)

    return top_df


def top_cumulative(df, _topn):
    # 计算区间涨跌幅
    df0 = df.copy()
    t_df = df0.groupby(['symbol', 'start_time', '持仓方向']).agg({
        'end_time': 'max',
        'open': 'first',
        'close': 'last'
    }).reset_index()
    t_df = t_df.rename(columns={'open': '开仓价格', 'close': '清仓价格'})
    t_df['区间涨跌幅_%'] = (t_df['清仓价格'] / t_df['开仓价格'] - 1) * t_df['持仓方向'] * 100

    # 计算每个币种的涨跌幅累计值
    cumulative_df = t_df.groupby('symbol').agg({'区间涨跌幅_%': 'sum'}).reset_index()

    # 根据累计涨跌幅筛选出top N币种（最大和最小）
    top_cumulative_max = cumulative_df.nlargest(_topn, '区间涨跌幅_%')['symbol']
    top_cumulative_min = cumulative_df.nsmallest(_topn, '区间涨跌幅_%')['symbol']

    # 获取这些币种的所有单区间涨跌幅数据
    top_symbols = pd.concat([top_cumulative_max, top_cumulative_min], axis=0)
    top_intervals_df = t_df[t_df['symbol'].isin(top_symbols)]

    # 将累计涨跌幅信息合并到单区间数据中
    top_intervals_df = top_intervals_df.merge(cumulative_df, on='symbol', suffixes=('', '_累计'))

    # 重命名累计涨跌幅列
    top_intervals_df.rename(columns={'区间涨跌幅_%_累计': '累计区间涨跌幅_%'}, inplace=True)

    # 按照单区间涨跌幅进行排序
    top_intervals_df.sort_values(by=['累计区间涨跌幅_%', '区间涨跌幅_%'], ascending=[True, True], inplace=True)
    top_intervals_df.reset_index(drop=True, inplace=True)

    return top_intervals_df


def top_long_short(df, save_path, ascending=True):
    # 计算区间涨跌幅
    df0 = df.copy()
    t_df = df0.groupby(['symbol', 'start_time', '持仓方向']).agg({
        'end_time': 'max',
        'open': 'first',
        'close': 'last'
    }).reset_index()
    t_df = t_df.rename(columns={'open': '开仓价格', 'close': '清仓价格'})
    t_df['区间涨跌幅_%'] = (t_df['清仓价格'] / t_df['开仓价格'] - 1) * t_df['持仓方向'] * 100
    t_df = t_df[['symbol', '区间涨跌幅_%', '持仓方向', 'start_time', 'end_time', '开仓价格', '清仓价格']]

    # 统计做多和做空的盈亏情况
    long_short_summary = t_df.groupby('持仓方向').agg({'区间涨跌幅_%': 'sum'}).rename(columns={'区间涨跌幅_%': '累计区间涨跌幅_%'}).reset_index()
    long_short_summary.to_csv(f"{save_path}/多空盈亏统计.csv", index=False)

    # 统计做多的所有盈亏区间
    long_intervals = t_df[t_df['持仓方向'] == 1.0].sort_values(by='区间涨跌幅_%', ascending=ascending)
    long_intervals.to_csv(f"{save_path}/多头区间盈亏统计.csv", index=False)

    # 统计做空的所有盈亏区间
    short_intervals = t_df[t_df['持仓方向'] == -1.0].sort_values(by='区间涨跌幅_%', ascending=ascending)
    short_intervals.to_csv(f"{save_path}/空头区间盈亏统计.csv", index=False)

    # 自定义聚合函数，用于创建开仓区间字符串
    def create_interval_string(group):
        return ', '.join(group)

    t_df['开仓区间'] = t_df.apply(lambda row: f"[{row['start_time']}, {row['end_time']}]", axis=1)

    # 统计做多的所有币种的累计盈亏信息
    long_cumulative = t_df[t_df['持仓方向'] == 1.0].groupby('symbol').agg({
        '区间涨跌幅_%': 'sum',
        'start_time': 'count',
        '开仓区间': create_interval_string
    }).rename(columns={'start_time': '开仓次数', '区间涨跌幅_%': '累计区间涨跌幅_%'}).reset_index()
    long_cumulative.sort_values(by='累计区间涨跌幅_%', ascending=ascending).to_csv(f"{save_path}/多头币种盈亏统计.csv", index=False)

    # 统计做空的所有币种的累计盈亏信息
    short_cumulative = t_df[t_df['持仓方向'] == -1.0].groupby('symbol').agg({
        '区间涨跌幅_%': 'sum',
        'start_time': 'count',
        '开仓区间': create_interval_string
    }).rename(columns={'start_time': '开仓次数', '区间涨跌幅_%': '累计区间涨跌幅_%'}).reset_index()
    short_cumulative.sort_values(by='累计区间涨跌幅_%', ascending=ascending).to_csv(f"{save_path}/空头币种盈亏统计.csv", index=False)


def merge_and_draw(top_df, df, _start, _end, _factor_name, _back_hour, _topn, _save_path, _extra_factors=None,
                   _is_draw_rank=False):
    """
    合并并绘制币种k线和因子图(会绘制_start至_end之间的，日期不全是因为币下架了，要求该币种包含在持仓面板中)
    如果_is_draw_rank=True，则增加一个子图用于绘制因子排名，因子排名相关配置信息不在函数中传入，直接引入"盈亏分析器.py"中的定义
    """
    _factors = [[_factor_name, _back_hour]]
    if _extra_factors is not None:
        _factors = _factors + _extra_factors
    _df1, _df2, symbols_rise_fall_rank, symbols_factors_rank = None, None, None, None
    if _is_draw_rank:
        _symbol_list = top_df['symbol'].unique().tolist()
        print("_symbol_list", _symbol_list)
        # 计算过滤后的币种
        _df1, _df2 = get_symbols_with_filter_before(_start, _end, filter_before_params)
        # 计算涨跌幅榜排名
        symbols_rise_fall_rank = calculate_symbols_rise_fall_rank(_start, _end, _symbol_list, rise_fall_factor,
                                                                  hold_period, _df1, _df2)
        # 计算因子值排名
        symbols_factors_rank = calculate_symbols_factor_list_rank(_start, _end, _symbol_list, _factors, _df1, _df2)

    # 合并topn的因子值
    if _extra_factors is None:  # 当非None时，应为[['factor_name1', back_hour1], ['factor_name2', back_hour2], ...]
        _extra_factors = []
    group = top_df.groupby(['symbol'])
    for symbol, _df in group:
        print("symbol:", symbol)
        symbol0 = symbol.replace('USDT', '-USDT')
        # 读取合并因子值数据
        draw_df = pd.read_feather(os.path.join(pickle_data, symbol0 + '.pkl'))
        for i, _factor in enumerate(_factors):
            if (not isinstance(_factor[0], str)) or (not isinstance(_factor[1], int)):
                raise Exception(f'因子值[{_factor}]不符合规范([因子名, 因子参数])')
            factor_df = pd.read_feather(os.path.join(factor_pickle, symbol0, 'coin_alpha_factor_' + f'{_factor[0]}' + '.pkl'))
            draw_df[f'{_factor[0]}_bh_{_factor[1]}'] = factor_df[f'{_factor[0]}_bh_{_factor[1]}']
        df0 = df[['candle_begin_time', 'symbol', '开平状态']]
        draw_df = pd.merge(draw_df, df0, on=['candle_begin_time', 'symbol'], how='left')
        if _is_draw_rank:
            symbol1 = symbol0.replace('-USDT', 'USDT')
            # 获取涨跌幅榜的排名数据(如果不为None)
            rise_fall_rank = symbols_rise_fall_rank[symbols_rise_fall_rank['symbol'] == symbol1]
            # 获取各因子值的排名数据(如果不为None)
            factors_rank = symbols_factors_rank[symbols_factors_rank['symbol'] == symbol1]
            rank_df = pd.merge(rise_fall_rank, factors_rank, on='candle_begin_time', how='outer')
            draw_df = pd.merge(draw_df, rank_df, on='candle_begin_time', how='outer')

            # 计算每个时刻过滤后多/空总币种数，用于显示多空涨跌幅榜/因子排名时，按照"pos_rank/total-pos_rank"的方式显示
            long_symbol_count = _df1.groupby('candle_begin_time')['symbol'].nunique().reset_index(
                name='long_symbol_num')
            short_symbol_count = _df2.groupby('candle_begin_time')['symbol'].nunique().reset_index(
                name='short_symbol_num')
            # 合并到一个新的 DataFrame
            symbol_count = pd.merge(long_symbol_count, short_symbol_count, on='candle_begin_time', how='outer')
            draw_df = pd.merge(draw_df, symbol_count, on='candle_begin_time', how='outer')

        # [计算涨跌幅]
        draw_df['pct_chg'] = df['close'].pct_change()
        # [绘制全部时段k线图]
        draw_df = draw_df[draw_df['candle_begin_time'] >= pd.to_datetime(_start)]
        draw_df = draw_df[draw_df['candle_begin_time'] <= pd.to_datetime(_end)]
        mark_point_list = []
        for index, row in draw_df.iterrows():
            x = row['candle_begin_time']
            y = row['high']
            signal = row['开平状态']
            # 当状态是开仓或者平仓时添加标记
            if signal in ['开空', '开多', '平空', '平多']:
                mark_point_list.append({
                    'x': x,
                    'y': y,
                    'showarrow': True,
                    'text': signal,
                    'arrowside': 'end',
                    'arrowhead': 6,
                })

        _fig_config = [4, [[{"secondary_y": True}], [{}], [{}], [{}]], [4, 1, 1, 1]] if _is_draw_rank else [3, [[{"secondary_y": True}], [{}], [{}]], [4, 1, 1]]
        # 创建子图
        fig = make_subplots(rows=_fig_config[0],
                            cols=1,
                            shared_xaxes=True,
                            vertical_spacing=0.02,
                            specs=_fig_config[1],  # 添加次级Y轴
                            row_heights=_fig_config[2])

        # 设置图表布局
        fig.update_layout(
            hovermode='x unified',  # 设置统一的悬停模式
            hoverlabel=dict(font_size=10),  # 悬停字体大小
        )

        # 添加 K 线图
        candlestick = go.Candlestick(x=draw_df['candle_begin_time'],
                                     open=draw_df['open'],
                                     high=draw_df['high'],
                                     low=draw_df['low'],
                                     close=draw_df['close'],
                                     name=f'{symbol0}_{_factor_name}',
                                     increasing_line_color='#008080',
                                     decreasing_line_color='#DC143C',
                                     hovertext=draw_df['pct_chg'].apply(lambda x: f'pct: {x*100:.2f}%'))
        fig.add_trace(candlestick, row=1, col=1, secondary_y=False)

        # 绘制多个因子值
        for i, _factor in enumerate(_factors):
            factor_trace = go.Scatter(
                x=draw_df['candle_begin_time'],
                y=draw_df[f'{_factor[0]}_bh_{_factor[1]}'],
                mode='lines',
                name=f'因子值：{_factor[0]}_bh_{_factor[1]}')
            fig.add_trace(factor_trace, row=1, col=1, secondary_y=True)

        # 以下为新增的盈亏计算逻辑 [计算有问题,应该匹配每一对开平仓信号]
        draw_df['profit_loss'] = 0
        open_position = None
        for index, row in draw_df.iterrows():
            signal = row['开平状态']
            if signal in ['开空', '开多']:
                # 记录开仓价格和类型
                open_position = {'price': row['close'], 'type': signal, 'open_index': index}
            elif signal in ['平空', '平多'] and open_position:
                # 从开仓到平仓之间的每个时间点上更新盈亏百分比
                for i in range(open_position['open_index'], index + 1):
                    if open_position['type'] == '开多':
                        draw_df.at[i, 'profit_loss'] = ((draw_df.at[i, 'close'] - open_position['price']) /
                                                        open_position['price']) * 100
                    elif open_position['type'] == '开空':
                        draw_df.at[i, 'profit_loss'] = ((open_position['price'] - draw_df.at[i, 'close']) /
                                                        open_position['price']) * 100
                open_position = None

        colors = [
            '#008080',  # 青色
            '#00CED1',  # 暗青色
            '#8B0000',  # 深红
            '#CD5C5C',  # 印度红
            '#4169E1',  # 皇家蓝
            '#8A2BE2',  # 紫罗兰
            '#DA70D6',  # 兰花紫
            '#FF69B4',  # 热情的粉红
            '#20B2AA',  # 浅海洋绿
            '#4682B4',  # 钢青
            '#6B8E23',  # 橄榄褐
            '#DB7093'  # 苍白的紫罗兰红
        ]

        if _is_draw_rank:
            # 添加涨跌幅排名和因子值排名的折线图
            _factor_list_with_rff = [[rise_fall_factor, hold_period]] + _factors
            for i, _factor in enumerate(_factor_list_with_rff):
                long_rank_trace = go.Scatter(
                    x=draw_df['candle_begin_time'],
                    y=draw_df[f'long_{_factor[0]}_bh_{_factor[1]}_rank'],
                    mode='lines+markers',
                    name=f'long_{_factor[0]}_bh_{_factor[1]}_rank',
                    line=dict(color=colors[2 * i % len(colors)])
                )
                fig.add_trace(long_rank_trace, row=2, col=1)

                short_rank_trace = go.Scatter(
                    x=draw_df['candle_begin_time'],
                    y=draw_df[f'short_{_factor[0]}_bh_{_factor[1]}_rank'],
                    mode='lines+markers',
                    name=f'short_{_factor[0]}_bh_{_factor[1]}_rank',
                    line=dict(color=colors[(2 * i + 1) % len(colors)])
                )
                fig.add_trace(short_rank_trace, row=2, col=1)

            # 在折线图中添加总数
            long_num_trace = go.Scatter(
                x=draw_df['candle_begin_time'],
                y=draw_df['long_symbol_num'],
                mode='lines',
                name='long_total',
                line=dict(color=colors[-4])
            )
            fig.add_trace(long_num_trace, row=2, col=1)

            short_num_trace = go.Scatter(
                x=draw_df['candle_begin_time'],
                y=draw_df['short_symbol_num'],
                mode='lines',
                name='short_total',
                line=dict(color=colors[-3])
            )
            fig.add_trace(short_num_trace, row=2, col=1)

            # 在折线图中添加top_n分割线
            draw_df['top_n'] = top_n
            long_num_trace = go.Scatter(
                x=draw_df['candle_begin_time'],
                y=draw_df['top_n'],
                mode='lines',
                name='long_top_n',
                showlegend=False,  # 不显示在图例中
                line=dict(color=colors[-2])
            )
            fig.add_trace(long_num_trace, row=2, col=1)

            short_num_trace = go.Scatter(
                x=draw_df['candle_begin_time'],
                y=draw_df['short_symbol_num'] - top_n,
                mode='lines',
                name='short_top_n',
                showlegend=False,  # 不显示在图例中
                line=dict(color=colors[-1])
            )
            fig.add_trace(short_num_trace, row=2, col=1)

        # def get_color(value):
        #     if value >= 20:
        #         color = '#008080'
        #     elif (value >= 10)&(value < 20):
        #         color = '#20B2AA'
        #     elif (value >= 5)&(value < 10):
        #         color  = '#00CED1'
        #     elif (value >= 0)&(value < 10):
        #         color = '#40E0D0'
        #     elif (value >= -5)&(value < 0):
        #         color = '#F08080'
        #     elif (value >= -10)&(value < -5):
        #         color = '#CD5C5C'
        #     elif (value >= -20)&(value < -10):
        #         color = '#A52A2A'
        #     else:
        #         color = '#8B0000'
        #     return color

        def get_color_inc(ind,name):
            if draw_df[name].iloc[ind] >= 0:
                if draw_df[name].iloc[ind]/draw_df[name].iloc[ind-1] > 1:
                    color = '#008080'
                else:
                    color = '#00CED1'
            else:
                if draw_df[name].iloc[ind]/draw_df[name].iloc[ind-1] > 1:
                    color = '#8B0000'
                else:
                    color = '#CD5C5C'
            return color

        # 创建盈亏柱状图 [柱状图范围应该横跨开平仓]
        profit_loss_bar = go.Bar(
            x=draw_df['candle_begin_time'],
            y=draw_df['profit_loss'],
            name='盈亏(%)',
            marker_color=[get_color_inc(ind,'profit_loss') for ind in range(len(draw_df))]
        )

        # 添加成交额图
        volume_bar = go.Bar(x=draw_df['candle_begin_time'],
                            y=draw_df['quote_volume'],
                            name='成交额',
                            marker_color=[get_color_inc(ind,'quote_volume') for ind in range(len(draw_df))]
                            )

        if _is_draw_rank:
            fig.add_trace(profit_loss_bar, row=3, col=1)
            fig.add_trace(volume_bar, row=4, col=1)
        else:
            fig.add_trace(profit_loss_bar, row=2, col=1)
            fig.add_trace(volume_bar, row=3, col=1)

        # 设置图表布局
        fig.update_layout(title=f'{_factor_name}_bh_{_back_hour}',
                          xaxis_rangeslider_visible=False,
                          plot_bgcolor='#F5F5F5',
                          )
        # 设置文字标识
        fig.update_layout(annotations=mark_point_list, title=f'{_factor_name}_bh_{_back_hour}', font=dict(size=16, color='#4169E1'))

        # 设置title
        fig.update_yaxes(title_text="K线", row=1, col=1)
        fig.update_yaxes(title_text="因子值", row=1, col=1, secondary_y=True)
        if _is_draw_rank:
            fig.update_yaxes(title_text="因子排名", row=2, col=1, autorange='reversed')
            print('reversed')
            fig.update_yaxes(title_text="盈亏(%)", row=3, col=1)
            fig.update_yaxes(title_text="成交额", row=4, col=1)
        else:
            fig.update_yaxes(title_text="盈亏(%)", row=2, col=1)
            fig.update_yaxes(title_text="成交额", row=3, col=1)

        start_symbol = datetime.strptime(_start, "%Y-%m-%d %H:%M:%S").strftime("%Y%m%d")
        end_symbol = datetime.strptime(_end, "%Y-%m-%d %H:%M:%S").strftime("%Y%m%d")
        # fig.write_html(os.path.join(_save_path, f'{symbol0}_{start_symbol}_{end_symbol}.html'))
        # 强制设定编码为UTF-8
        with open(os.path.join(_save_path, f'{symbol0}_{start_symbol}_{end_symbol}.html'), "w", encoding="utf8") as file:
            write_html(fig, file)

    return


def merge_and_draw_without_position(_symbol, _start, _end, _factor_name, _back_hour, _topn, _save_path, _extra_factors=None):
    """
    合并并绘制币种k线和因子图(会绘制_start至_end之间的，日期不全是因为币下架了，要求该币种包含在持仓面板中)
    """
    # 合并topn的因子值
    if _extra_factors is None:  # 当非None时，应为[['factor_name1', back_hour1], ['factor_name2', back_hour2], ...]
        _extra_factors = []
    _factors = [[_factor_name, _back_hour]] + _extra_factors
    print("symbol:", _symbol)
    symbol0 = _symbol + '-USDT'
    # 读取合并因子值数据
    draw_df = pd.read_feather(os.path.join(pickle_data, symbol0 + '.pkl'))
    for i, _factor in enumerate(_factors):
        if (not isinstance(_factor[0], str)) or (not isinstance(_factor[1], int)):
            raise Exception(f'因子值[{_factor}]不符合规范([因子名, 因子参数])')
        factor_df = pd.read_feather(os.path.join(factor_pickle, symbol0, 'coin_alpha_factor_' + f'{_factor[0]}' + '.pkl'))
        draw_df[f'{_factor[0]}_bh_{_factor[1]}'] = factor_df[f'{_factor[0]}_bh_{_factor[1]}']
    # [绘制全部时段k线图]
    draw_df = draw_df[draw_df['candle_begin_time'] >= pd.to_datetime(_start)]
    draw_df = draw_df[draw_df['candle_begin_time'] <= pd.to_datetime(_end)]

    # 创建子图
    fig = make_subplots(rows=2,
                        cols=1,
                        shared_xaxes=True,
                        vertical_spacing=0.02,
                        specs=[[{"secondary_y": True}], [{}]],  # 添加次级Y轴
                        row_heights=[4, 1])

    # 添加 K 线图
    candlestick = go.Candlestick(x=draw_df['candle_begin_time'],
                                 open=draw_df['open'],
                                 high=draw_df['high'],
                                 low=draw_df['low'],
                                 close=draw_df['close'],
                                 name=f'{symbol0}_{_factor_name}',
                                 increasing_line_color='green',
                                 decreasing_line_color='red')
    fig.add_trace(candlestick, row=1, col=1, secondary_y=False)

    # 绘制多个因子值
    for i, _factor in enumerate(_factors):
        factor_trace = go.Scatter(
            x=draw_df['candle_begin_time'],
            y=draw_df[f'{_factor[0]}_bh_{_factor[1]}'],
            mode='lines',
            name=f'因子值：{_factor[0]}_bh_{_factor[1]}')
        fig.add_trace(factor_trace, row=1, col=1, secondary_y=True)

    # 添加成交额图
    volume_bar = go.Bar(x=draw_df['candle_begin_time'],
                        y=draw_df['quote_volume'],
                        name='成交额',
                        marker_color='green'
                        )

    fig.add_trace(volume_bar, row=2, col=1)

    # 设置图表布局
    fig.update_layout(title=f'{_factor_name}_bh_{_back_hour}',
                      xaxis_rangeslider_visible=False)
    # 设置文字标识
    fig.update_layout(title=f'{_factor_name}_bh_{_back_hour}',
                      font=dict(size=16, color='blue'))

    # 设置title
    fig.update_yaxes(title_text="K线", row=1, col=1)
    fig.update_yaxes(title_text="因子值", row=1, col=1, secondary_y=True)
    fig.update_yaxes(title_text="成交额", row=2, col=1)

    # fig.show()
    # 保存图片
    start_symbol = datetime.strptime(_start, "%Y-%m-%d %H:%M:%S").strftime("%Y%m%d")
    end_symbol = datetime.strptime(_end, "%Y-%m-%d %H:%M:%S").strftime("%Y%m%d")
    # fig.write_html(os.path.join(_save_path, f'{symbol0}_{start_symbol}_{end_symbol}.html'))
    # 强制设定编码为UTF-8
    with open(os.path.join(_save_path, f'{symbol0}_{start_symbol}_{end_symbol}.html'), "w", encoding="utf8") as file:
        write_html(fig, file)

    return


def f1_TopN_analysis(_start, _end, _compound_name, _factor_name, _back_hour, _period, _topn, if_cumulative_top: bool = True,
                     if_draw: bool = False, if_copy_factor: bool = False, extra_save_suffix="", extra_factors=None,
                     is_draw_rank=False):
    """
    分析某个因子在某个回测时段的盈亏TopN的币种，并绘制对应币种在该时段的k线和因子值(if_draw=True)
    :param _start: 因子分析开始时间
    :param _end: 因子分析结束时间
    :param _factor_name: 因子名
    :param _back_hour: 因子参数
    :param _period: hold_hour，如'1H'
    :param _topn: 要分析的单时段盈亏TopN的币种数
    :param if_cumulative_top: 是否统计累计盈亏top币种，True: 统计累计盈亏top币种并输出对应币种的所有开仓时段盈亏，False: 统计单区间盈亏top并输出对应币种top时段盈亏
    :param if_draw: 是否输出TopN币种的k线和因子值
    :param if_copy_factor: 是否保存回测因子到目标目录下
    :param extra_save_suffix: 保存路径的额外后缀，用于在对于单个因子进行优化时同时保存多个版本因子的回测分析结果
    :param extra_factors: 用于同时绘制多个因子值，格式应为[['factor_name1', back_hour1], ['factor_name2', back_hour2], ...]
    :param is_draw_rank: 是否增加一个子图用于绘制因子排名
    """
    # 合并数据并标记单笔交易
    df = merge_and_mark(_start, _end, _compound_name, _period)
    # 计算多空总币数
    long_symbol_num = df[df['持仓方向'] == 1]['symbol'].nunique()
    short_symbol_num = df[df['持仓方向'] == -1]['symbol'].nunique()

    # 创建文件保存路径
    save_path = os.path.join(root_path, 'data/回测结果', '中性回放结果', f'{_compound_name}')  # 回测查看文件夹
    if not os.path.exists(save_path):
        os.mkdir(save_path)
    if if_cumulative_top:
        save_path = os.path.join(save_path, f'{_factor_name}_bh_{_back_hour}_Top{_topn}_Cumulative' + extra_save_suffix)  # 累计盈亏top
    else:
        save_path = os.path.join(save_path, f'{_factor_name}_bh_{_back_hour}_Top{_topn}_Interval' + extra_save_suffix)  # 单区间盈亏top
    if not os.path.exists(save_path):
        os.mkdir(os.path.join(save_path))

    # 计算收益
    if if_cumulative_top:
        top_df = top_cumulative(df, _topn)
        top_df.to_csv(os.path.join(save_path, f'Top{_topn}累计盈亏币种分析.csv'), encoding='gbk')
    else:
        top_df = top_interval(df, _topn)
        top_df.to_csv(os.path.join(save_path, f'Top{_topn}区间盈亏币种分析.csv'), encoding='gbk')

    # 合并topn的因子值并绘图
    if if_draw:
        merge_and_draw(top_df, df, _start, _end, _factor_name, _back_hour, _topn, save_path, _extra_factors=extra_factors,
                       _is_draw_rank=is_draw_rank)

    if if_copy_factor:
        shutil.copy2(
            os.path.join(root_path, 'program/factors', f'{_factor_name}.py'),  # from
            os.path.join(save_path, f'{_factor_name}.py'),  # to
        )

    print("===> 该回测时段内[做多]币种总数:", long_symbol_num)
    print("===> 该回测时段内[做空]币种总数:", short_symbol_num)
    print(f"===> 该回测时段内[累计/区间]涨跌幅top{_topn}的币种数:", top_df['symbol'].nunique())


def f1_single_symbol_analysis(_start, _end, _compound_name, _factor_name, _back_hour, _period, _symbol, if_copy_factor: bool = False,
                              extra_save_suffix="", extra_factors=None, is_draw_rank=False):
    """
    绘制某个因子在某个回测时段下，某个币种的k线和因子
    :param _start: 因子分析开始时间
    :param _end: 因子分析结束时间
    :param _compound_name: 组份名
    :param _factor_name: 因子名
    :param _back_hour: 因子参数
    :param _period: hold_hour，如'1H'
    :param _symbol: 要分析的单个币种
    :param if_copy_factor: 是否保存回测因子到目标目录下
    :param extra_save_suffix: 保存路径的额外后缀，用于在对于单个因子进行优化时同时保存多个版本因子的回测分析结果
    :param extra_factors: 用于同时绘制多个因子值，格式应为[['factor_name1', back_hour1], ['factor_name2', back_hour2], ...]
    :param is_draw_rank: 是否增加一个子图用于绘制因子排名
    """
    # 合并数据并标记单笔交易
    df = merge_and_mark(_start, _end, _compound_name, _period, _symbol)

    # 创建文件保存路径
    save_path = os.path.join(root_path, 'data/回测结果', '中性回放结果', f'{_compound_name}')  # 回测查看文件夹
    if not os.path.exists(save_path):
        os.mkdir(save_path)
    save_path = os.path.join(save_path, f'{_factor_name}_bh_{_back_hour}_{_symbol}' + extra_save_suffix)
    if not os.path.exists(save_path):
        os.mkdir(os.path.join(save_path))

    if not df.empty:
        top_df = top_cumulative(df, 1)
        top_df.to_csv(os.path.join(save_path, f'{_symbol}币种分析.csv'), encoding='gbk')
        merge_and_draw(top_df, df, _start, _end, _factor_name, _back_hour, 1, save_path, _extra_factors=extra_factors,
                       _is_draw_rank=is_draw_rank)
    else:
        print(f'币种[{_symbol}]的输出曲线将不包含买卖点和盈亏信息，并且不会输出币种盈亏分析结果')
        merge_and_draw_without_position(_symbol, _start, _end, _factor_name, _back_hour, 1, save_path, _extra_factors=extra_factors)

    if if_copy_factor:
        if if_copy_factor:
            shutil.copy2(
                os.path.join(root_path, 'program/factors', f'{_factor_name}.py'),  # from
                os.path.join(save_path, f'{_factor_name}.py'),  # to
            )


def f1_long_short_analysis(_start, _end, _compound_name, _factor_name, _back_hour, _period, ascending: bool = True,
                           if_copy_factor: bool = False, extra_save_suffix=""):
    """
    统计某个策略回放的多空盈亏信息并输出，包括：多头和空头的整体盈亏、多/空头所有区间盈亏、多/空头所有币种累计盈亏统计
    :param _start: 因子分析开始时间
    :param _end: 因子分析结束时间
    :param _factor_name: 因子名
    :param _back_hour: 因子参数(这里仅用于创建包含back_hour的目录)
    :param _period: hold_hour，如'1H'
    :param ascending: 后续统计输出时按照区间涨跌幅的升序或降序排列，默认升序
    :param if_copy_factor: 是否保存回测因子到目标目录下
    :param extra_save_suffix: 保存路径的额外后缀，用于在对于单个因子进行优化时同时保存多个版本因子的回测分析结果
    """
    # 合并数据并标记单笔交易
    df = merge_and_mark(_start, _end, _compound_name, _period)

    # 创建文件保存路径
    save_path = os.path.join(root_path, 'data/回测结果', '中性回放结果', f'{_compound_name}')  # 回测查看文件夹
    if not os.path.exists(save_path):
        os.mkdir(save_path)
    save_path = os.path.join(save_path, f'{_factor_name}_bh_{_back_hour}_多空盈亏分析' + extra_save_suffix)  # 单区间盈亏top
    if not os.path.exists(save_path):
        os.mkdir(os.path.join(save_path))

    # 计算多空盈亏统计并输出
    top_long_short(df, save_path, ascending)

    if if_copy_factor:
        shutil.copy2(
            os.path.join(root_path, 'program/factors', f'{_factor_name}.py'),  # from
            os.path.join(save_path, f'{_factor_name}.py'),  # to
        )
